1 inicio

3 MetodologĆ­a

El presente estudio siguió un enfoque de aprendizaje supervisado orientado a la clasificación binaria (pagado / incumplido). El procedimiento general consistió en aplicar dos familias de clasificadores (Logit y KNN) sobre datos previamente preparados, y evaluar su desempeño mediante métricas complementarias y procedimientos de validación.

La implementación se realizó en R empleando flujos estandarizados de preprocesamiento y modelado. Para reducir la influencia de escalas dispares y valores extremos, se aplicaron filtros sobre outliers relevantes y se normalizaron las variables numéricas (centrado y escala) cuando correspondió; dicho escalado fue especialmente crítico para KNN, dada su dependencia de distancias euclidianas. La muestra final fue construida de forma estratificada para asegurar balance entre las clases objetivo, y todas las operaciones aleatorias (muestreo y particionado); ademÔs, se fijó la semilla (set.seed(28)) para garantizar reproducibilidad.

En cuanto al ajuste de modelos, la regresión logĆ­stica se estimó mediante glm(…, family = binomial()), obteniendo probabilidades predictivas que permitieron tanto el anĆ”lisis de coeficientes como la construcción de curvas ROC y la determinación de umbrales operativos (Ć­ndice de Youden). El KNN se abordó de dos maneras; una implementación bĆ”sica con class::knn, evaluando k en un rango (k = 1:100) y seleccionando el k que maximizó la exactitud fuera de muestra; y una versión integrada en caret::train() que incorporó validación cruzada estratificada (5 folds), bĆŗsqueda automĆ”tica de hiperparĆ”metros (tuneLength) y optimización segĆŗn el AUC (mĆ©trica ROC), lo que permitió una selección de modelo mĆ”s robusta frente a la variabilidad de los datos.

3.1 Definición de las variables

Las variables utilizadas en el estudio se organizan en dos grupos: dependientes e independientes. La variable dependiente seleccionada es el estado de pago de la persona (Estado), construida a partir del indicador Default y se codificó como factor con niveles ā€œPagadoā€ (no default) y ā€œIncumplidoā€ (default). Esta variable refleja el resultado del contrato crediticio y es una medida directa del riesgo de incumplimiento, por lo que su correcta definición y codificación es central para cualquier ejercicio de scoring, ya que no solo indica la ocurrencia del impago, sino que tambiĆ©n sirve como referencia para estimar probabilidades de default y calibrar los umbrales de decisión en procesos de aprobación crediticia.

Entre las variables independientes se incorporaron predictores financieros y de propósito del préstamo que, según la teoría del riesgo y la prÔctica del credit scoring, guardan relación con la capacidad de pago y la propensión al incumplimiento. El ingreso anual declarado por el solicitante Ingreso se emplea como variable de la capacidad de repago; a mayor ingreso disponible se espera una menor probabilidad de default, dado que permite absorber obligaciones adicionales y enfrentar eventos adversos sin perder la capacidad de servicio de la deuda. No obstante, la medida declarativa del ingreso puede presentar sesgos por subdeclaración o variabilidad temporal, por lo que su interpretación debe hacerse con cuidado.

La Relacion deuda/ingreso sintetiza la carga financiera del solicitante al relacionar obligaciones vigentes con su ingreso. Un DTI (Debt-to-Income, que viene siendo la relación deuda/ingreso) elevado indica que una fracción significativa del ingreso ya estÔ comprometida con otras deudas, lo que incrementa la vulnerabilidad ante shocks y aumenta la probabilidad de incumplimiento. Se toma en cuenta ya que permite captar no solo la magnitud del endeudamiento sino también la presión relativa sobre la liquidez del hogar o individuo.

El monto solicitado Monto prestamo incorpora la dimensión contractual del crédito, que son los préstamos de mayor cuantía: los cuales, sin ajustes proporcionales en condiciones o capacidad de pago, tienden a elevar el riesgo de default por aumentar la carga mensual y alargar el horizonte de exposición. AdemÔs, el monto solicitado puede interactuar con otras variables (por ejemplo, ingreso o FICO) para dibujar perfiles diferenciados de riesgo. Su inclusión facilita distinguir situaciones en las que un mismo monto resulta asumible o riesgoso según el contexto financiero del solicitante.

El puntaje crediticio Puntaje FICO funciona como un indicador consolidado del historial crediticio y de la probabilidad observada de cumplimiento en períodos previos. Puntajes mÔs altos se asocian sistemÔticamente con menor probabilidad de impago, pues reflejan comportamientos de pago estables, menor incidencia de morosidad previa y hÔbitos financieros mÔs conservadores. Por su carÔcter informativo y su uso extendido en la industria, el puntaje FICO aporta a la discriminación del riesgo y suele mostrar efectos significativos en modelos paramétricos y no paramétricos.

El propósito del préstamo reagrupado Proposito agrupado captura el destino del crédito, como lo puede ser una consolidación de deuda, compra de vivienda o vehículo, inversión en negocio, educación. Refleja diferencias cualitativas en la naturaleza y prioridad del gasto. Distintos propósitos implican perfiles de riesgo heterogéneos, es decir, un préstamo para consolidación de deuda puede indicar una situación financiera tensionada, mientras que un préstamo para inversión productiva o educación puede asociarse a retornos que faciliten el repago.

variables_modelo <- data.frame(
  Variable = c("Estado", "Ingreso", "Relación deuda/ingreso",
               "Monto préstamo", "Puntaje FICO", "Propósito", "Binaria"),
  
  Descripción = c(
    "Variable dependiente que representa el resultado final del crƩdito.",
    "Ingreso anual declarado por el solicitante, indicador de capacidad de pago.",
    "Ratio financiero que mide la carga de endeudamiento frente al ingreso.",
    "Valor del prƩstamo solicitado por el cliente.",
    "Puntaje crediticio que resume el historial de crƩdito del solicitante.",
    "Motivo declarado del prƩstamo, agrupado en categorƭas mayores.",
    "Versión numérica de la variable dependiente usada en el modelo."
  ),
  
  Tipo_Variable = c(
    "Categórica (binaria)",
    "Cuantitativa continua",
    "Cuantitativa continua",
    "Cuantitativa continua",
    "Cuantitativa continua",
    "Categórica (agrupada)",
    "Binaria (numƩrica)"
  ),
  
  Ejemplos_Notas = c(
    "Ej: 'Pagado', 'Incumplido'.",
    "En dólares anuales.",
    "Proporción (ej. 0.35).",
    "Monto del prƩstamo en USD.",
    "Rango tĆ­pico: 300 – 850.",
    "Ej: 'Consolidación', 'Negocio', 'Otros'.",
    "0 = Paga, 1 = No paga."
  )
)


tabla_variables <- kable(
  variables_modelo,
  format = "html",
  col.names = c("Variable", "Descripción", "Tipo de Variable", "Ejemplos / Notas"),
  align = c('l', 'l', 'l', 'l'),
  caption = "Tabla 1. Variables utilizadas en el Modelo de Riesgo Crediticio"
) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE,
    font_size = 14,
    position = "center"
  ) %>%
  row_spec(0, background = "#990000", color = "white", bold = TRUE) %>%
  row_spec(1:7, background = "#FFFDF5") %>%
  column_spec(1, bold = TRUE, width = "3cm") %>%
  column_spec(2, width = "6cm") %>%
  column_spec(3, width = "3cm") %>%
  column_spec(4, width = "3.5cm") %>%
  footnote(
    general = "Elaboración propia con base en el dataset Lending Club (2007-2018).",
    general_title = "Fuente: ",
    footnote_as_chunk = TRUE
  )

tabla_variables
Tabla 1. Variables utilizadas en el Modelo de Riesgo Crediticio
Variable Descripción Tipo de Variable Ejemplos / Notas
Estado Variable dependiente que representa el resultado final del crĆ©dito. Categórica (binaria) Ej: ā€˜Pagado’, ā€˜Incumplido’.
Ingreso Ingreso anual declarado por el solicitante, indicador de capacidad de pago. Cuantitativa continua En dólares anuales.
Relación deuda/ingreso Ratio financiero que mide la carga de endeudamiento frente al ingreso. Cuantitativa continua Proporción (ej. 0.35).
Monto prƩstamo Valor del prƩstamo solicitado por el cliente. Cuantitativa continua Monto del prƩstamo en USD.
Puntaje FICO Puntaje crediticio que resume el historial de crĆ©dito del solicitante. Cuantitativa continua Rango tĆ­pico: 300 – 850.
Propósito Motivo declarado del prĆ©stamo, agrupado en categorĆ­as mayores. Categórica (agrupada) Ej: ā€˜Consolidación’, ā€˜Negocio’, ā€˜Otros’.
Binaria Versión numérica de la variable dependiente usada en el modelo. Binaria (numérica) 0 = Paga, 1 = No paga.
Fuente: Elaboración propia con base en el dataset Lending Club (2007-2018).

3.2 Base de datos

lending_raw <- read_csv("LC_loans_granting_model_dataset.csv", guess_max = 20000)

lending_base <- lending_raw %>%
  select(revenue, dti_n, loan_amnt, fico_n, Default, purpose, issue_d) %>%
  rename(
    ingreso = revenue,
    relacion_deuda_ingreso = dti_n,
    monto_prestamo = loan_amnt,
    puntaje_fico = fico_n,
    estado_pago = Default,
    proposito = purpose
  ) %>%
  mutate(
    fecha_emision = parse_date_time(issue_d, orders = "b-Y", locale = "en_US"),
    AƱo = year(fecha_emision),
    proposito = as.factor(proposito),
    proposito_agrupado = fct_collapse(
      proposito,
      Consolidacion = c("debt_consolidation", "credit_card"),
      "Casa/Vehiculo" = c("home_improvement", "major_purchase", "car", "house"),
      "Negocio/Estudio" = c("small_business", "educational")
    ),
    proposito_agrupado = fct_other(
      proposito_agrupado,
      keep = c("Consolidacion", "Casa/Vehiculo", "Negocio/Estudio"),
      other_level = "Otros"
    ),
    estado_pago = fct_recode(as.factor(estado_pago), "Pagado" = "0", "Incumplido" = "1")
  ) %>%
  select(-proposito, -issue_d, -fecha_emision) %>% 
  filter(ingreso <= 250000, relacion_deuda_ingreso <= 50) %>%
  drop_na()

set.seed(0408)


count_pagado <- sum(lending_base$estado_pago == "Pagado")
count_incumplido <- sum(lending_base$estado_pago == "Incumplido")

sample_size <- min(5000, count_pagado, count_incumplido)

paga <- lending_base %>% 
  filter(estado_pago == "Pagado") %>% 
  sample_n(sample_size)

nopaga <- lending_base %>% 
  filter(estado_pago == "Incumplido") %>% 
  sample_n(sample_size)

Base_datos <- bind_rows(paga, nopaga) %>% 
  select(AƱo, ingreso, relacion_deuda_ingreso, monto_prestamo, puntaje_fico, proposito_agrupado, estado_pago) %>%
  rename(
    Ingreso = ingreso,
    Relacion_deuda_ingreso = relacion_deuda_ingreso,
    Monto_prestado = monto_prestamo,
    FICO = puntaje_fico,
    Proposito = proposito_agrupado,
    Estado = estado_pago
  )

Base_datos <- Base_datos[sample(nrow(Base_datos)), ] %>% 
  mutate(Relacion_deuda_ingreso=Relacion_deuda_ingreso/100)


Base_datos_formateada <- Base_datos %>%
  mutate(
    Ingreso_formateado = scales::dollar(Ingreso, accuracy = 1),
    Monto_prestado_formateado = scales::dollar(Monto_prestado, accuracy = 1),
    Relacion_deuda_ingreso_formateado = scales::percent(Relacion_deuda_ingreso, accuracy = 0.1),
    FICO_formateado = as.integer(FICO)
  )

tabla_interactiva <- Base_datos %>%
  datatable(
    colnames = c(
      "AƱo",
      "Ingreso Anual (USD)", 
      "Relación Deuda/Ingreso", 
      "Monto del PrƩstamo (USD)",
      "Puntaje FICO", 
      "Propósito del Préstamo", 
      "Estado de Pago"
    ),
    filter = 'top',
    extensions = c('Buttons', 'Scroller'),
    options = list(
      pageLength = 10,
      dom = 'Bfrtip',
      scrollX = TRUE,
      scrollY = "400px",
      scroller = TRUE,
      buttons = list('copy', 'csv', 'excel', 'pdf', 'print'),
      language = list(
        url = '//cdn.datatables.net/plug-ins/1.10.25/i18n/Spanish.json'
      )
    ),
    caption = htmltools::tags$caption(
      style = 'caption-side: top; text-align: center; color: #FF0000; font-family: "Playfair Display"; font-size: 1.3rem; font-weight: bold;',
      'Tabla 2. Base de Datos de PrƩstamos - Dataset Lending Club'
    ),
    class = 'row-border stripe hover order-column',
    rownames = FALSE,
    width = '100%'
  ) %>%
  formatCurrency(
    columns = c('Ingreso', 'Monto_prestado'),
    currency = '$', digits = 0, before = TRUE
  ) %>%
  formatRound(
    columns = 'FICO',
    digits = 0
  ) %>%
  formatStyle(
    'Estado',
    target = 'row',
    backgroundColor = styleEqual('Incumplido', '#FFE6E6')
  ) %>%
  formatStyle(
    'Estado',
    backgroundColor = styleEqual('Incumplido', '#FF6B6B'),
    color = styleEqual('Incumplido', 'white'),
    fontWeight = styleEqual('Incumplido', 'bold')
  ) %>%
  formatStyle(
    'Relacion_deuda_ingreso',
    backgroundColor = styleInterval(
      cuts = c(30, 40), 
      values = c('white', '#FFF0F0', '#FFB8B8')
    )
  ) %>%
  formatStyle(
    'FICO',
    backgroundColor = styleInterval(
      cuts = c(600, 700),
      values = c('#FF6B6B', '#FFD8D8', 'white')
    )
  )


tabla_interactiva <- tabla_interactiva %>%
  htmlwidgets::prependContent(
    htmltools::tags$style(
      htmltools::HTML("
        table.dataTable thead th {
          background-color: #FF0000 !important;
          color: white !important;
          font-weight: bold !important;
        }
      ")
    )
  )

tabla_interactiva

4 Analisis descriptivo

El conjunto de datos analizado corresponde a registros de préstamos otorgados por la plataforma Lending Club, e incluye información relevante sobre las características financieras de los solicitantes y el comportamiento de pago asociado a cada crédito.

Con el fin de comprender de manera preliminar la composición y variabilidad de las observaciones, se presenta a continuación un resumen de las principales estadísticas descriptivas y la distribución de frecuencias por estado de pago.

resumen_general <- Base_datos %>%
  select(Ingreso, Relacion_deuda_ingreso, Monto_prestado, FICO) %>%
  mutate(Relacion_deuda_ingreso=Relacion_deuda_ingreso*100) %>% 
  rename("Relacion deuda/ingreso"=2,"Monto prestado"=3) %>% 
  summarise_all(list(
    n = ~sum(!is.na(.)),
    Media = ~mean(., na.rm = TRUE),
    Mediana = ~median(., na.rm = TRUE),
    sd = ~sd(., na.rm = TRUE),
    Minimo = ~min(., na.rm = TRUE),
    Maximo = ~max(., na.rm = TRUE)
  )) %>%
  pivot_longer(
    cols = everything(),
    names_to = c("variable", ".value"),
    names_pattern = "^(.*)_(n|Media|Mediana|sd|Minimo|Maximo)$"
  ) %>% 
  mutate(Media=round(Media,2),
         Mediana=round(Mediana,0),
         sd=round(sd,0),
         Maximo=round(Maximo,0))

4.1 EstadĆ­sticas descriptivas de las variables principales

La Tabla 1 resume las medidas de tendencia central y dispersión de las variables cuantitativas incluidas en el estudio.

tabla_resumen = resumen_general %>%
  select(-n) %>%  
  kable(
    format = "html",
    col.names = c("Variable", "Media", "Mediana", "Desv. EstƔndar", "Mƭnimo", "MƔximo"),
    align = c('l', 'r', 'r', 'r', 'r', 'r'),
    caption = "Tabla 1: Estadƭsticas Descriptivas de las Variables Principales del Dataset de PrƩstamos"
  ) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE,
    font_size = 14,
    position = "center"
  ) %>%
  row_spec(0, background = "#990000", color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE, width = "3cm") %>%
  column_spec(2, width = "2cm") %>%
  column_spec(3, width = "2cm") %>%
  column_spec(4, width = "2.5cm") %>%
  column_spec(5, width = "2cm") %>%
  column_spec(6, width = "2cm") %>%
  footnote(
    general = "Fuente: Elaboración propia con base en datos de Lending Club",
    general_title = "Nota:",
    footnote_as_chunk = TRUE
  )

tabla_resumen
Tabla 1: Estadƭsticas Descriptivas de las Variables Principales del Dataset de PrƩstamos
Variable Media Mediana Desv. EstƔndar Mƭnimo MƔximo
Ingreso 72542.71 64000 39073 7000 250000
Relacion deuda/ingreso 19.21 19 9 0 49
Monto prestado 14850.24 13000 8650 1000 40000
FICO 694.62 687 30 642 842
Nota: Fuente: Elaboración propia con base en datos de Lending Club

En promedio, los solicitantes reportan un ingreso anual de USD 72,543, con una mediana de USD 64,000, lo que sugiere una ligera asimetría positiva en la distribución, reflejando la presencia de algunos ingresos excepcionalmente altos que elevan el promedio.

La relación deuda/ingreso presenta una media de 19%, indicando que, en promedio, los deudores destinan cerca de una quinta parte de sus ingresos al pago de obligaciones financieras. No obstante, el rango entre 0% y 50% evidencia una amplia heterogeneidad en los niveles de endeudamiento entre los solicitantes.

El monto promedio de los préstamos asciende a USD 14,850, con una desviación estÔndar de aproximadamente USD 8,645, lo que denota variabilidad significativa en los montos solicitados, posiblemente asociada a diferencias en capacidad de pago o propósito del crédito.

Por su parte, el puntaje FICO, que refleja el historial crediticio de los solicitantes, presenta una media de 697 puntos, situĆ”ndose dentro de la categorĆ­a de ā€œbuen crĆ©ditoā€. Su baja desviación estĆ”ndar (31.7) indica una distribución relativamente concentrada, lo que sugiere que la mayorĆ­a de los clientes poseen un perfil crediticio estable.

En conjunto, estas estadísticas evidencian una población de solicitantes con ingresos moderados a altos, niveles de endeudamiento diversos y un comportamiento crediticio predominantemente positivo.

4.2 Distribución individual de variables numéricas

4.2.1 Histograma del ingreso

La Figura 1 presenta la distribución de la variable ingreso anual de los solicitantes de préstamo. El histograma evidencia una asimetría positiva, donde la mayoría de los individuos reportan ingresos entre USD 40,000 y USD 80,000, mientras que un grupo reducido alcanza valores considerablemente mÔs altos, superiores a los USD 150,000.

library(gifski)
library(magick)

crear_frame <- function(percentil) {
  datos_filtrados <- Base_datos %>% 
    filter(Ingreso <= quantile(Base_datos$Ingreso, percentil, na.rm = TRUE))
  
  ggplot(datos_filtrados, aes(x = Ingreso)) +
    geom_histogram(
      binwidth = 5000,
      fill = SC["primary_red"],
      color = "white",
      alpha = 0.9
    ) +
    geom_vline(
      xintercept = median(Base_datos$Ingreso, na.rm = TRUE),
      color = SC["gold"],
      linetype = "dashed", 
      linewidth = 1.2
    ) +
    geom_vline(
      xintercept = mean(Base_datos$Ingreso, na.rm = TRUE),
      color = SC["dark_red"],
      linetype = "solid",
      linewidth = 0.8
    ) +
    scale_x_continuous(
      labels = scales::dollar_format(prefix = "$", big.mark = ","),
      limits = c(0, max(Base_datos$Ingreso, na.rm = TRUE))
    ) +
    scale_y_continuous(labels = scales::comma_format()) +
    labs(
      title = paste("Figura 1, Distribución del Ingreso -", round(percentil * 100), "% de datos"),
      x = "Ingreso Anual (USD)",
      y = "Frecuencia"
    ) +
    TS()
}


percentiles <- seq(0.1, 1, by = 0.1)
frames <- map(percentiles, crear_frame)


temp_files <- map_chr(seq_along(frames), function(i) {
  archivo <- tempfile(fileext = ".png")
  ggsave(archivo, frames[[i]], width = 10, height = 6, dpi = 100)
  archivo
})


animacion <- image_read(temp_files) %>%
  image_animate(fps = 2) %>%
  image_write("animacion_ingreso.gif")


knitr::include_graphics("animacion_ingreso.gif")

La línea roja punteada indica la mediana, ubicada alrededor de USD 65,000, lo cual coincide con la tendencia central observada en la tabla descriptiva. Esta concentración en niveles intermedios de ingreso sugiere que la base de datos estÔ compuesta principalmente por personas con capacidad de pago media, probablemente pertenecientes a segmentos laborales formales o con ingresos estables.

Los valores mÔs altos de ingreso, aunque minoritarios, representan a solicitantes con mayor capacidad financiera, lo que puede influir positivamente en su probabilidad de aprobación y cumplimiento del crédito. En conjunto, la distribución muestra una población heterogénea, pero con predominio de ingresos medios dentro del conjunto analizado.

4.2.2 Distribución de la relación deuda/ingreso

La Figura 2 ilustra la densidad de la variable relación deuda/ingreso (%), la cual mide el nivel de endeudamiento de los solicitantes respecto a su capacidad económica.

La distribución presenta una forma ligeramente asimétrica hacia la derecha, con un claro punto de concentración entre los 10% y 25%, y una mediana cercana al 18% (línea roja punteada).

mediana_dti <- median(Base_datos$Relacion_deuda_ingreso, na.rm = TRUE) * 100
media_dti <- mean(Base_datos$Relacion_deuda_ingreso, na.rm = TRUE) * 100

# Crear variable temporal para el plotting (multiplicada por 100)
Base_datos$dti_porcentaje <- Base_datos$Relacion_deuda_ingreso * 100

# Calcular densidad con los valores en porcentaje
densidad_dti <- density(Base_datos$dti_porcentaje, na.rm = TRUE)
max_densidad <- max(densidad_dti$y) * nrow(Base_datos) * 2

p_dti_interactivo <- plot_ly(Base_datos, x = ~dti_porcentaje) %>%
  add_histogram(
    name = "Frecuencia",
    nbinsx = 30,
    marker = list(
      color = SC["primary_red"],
      line = list(color = "white", width = 1)
    ),
    opacity = 0.7,
    hovertemplate = "DTI: %{x:.1f}%<br>Frecuencia: %{y}<extra></extra>"
  ) %>%
  add_lines(
    x = densidad_dti$x,
    y = densidad_dti$y * nrow(Base_datos) * 2,
    name = "Densidad",
    line = list(
      color = SC["secondary_red"],
      width = 2
    ),
    fill = 'tozeroy',
    fillcolor = paste0(SC["accent_red"], '40'),
    hovertemplate = "DTI: %{x:.1f}%<br>Densidad: %{y:.3f}<extra></extra>"
  ) %>%
  layout(
    title = list(
      text = "<b>Figura 2. Distribución de la Relación Deuda/Ingreso (DTI)</b>",
      font = list(
        family = "Playfair Display",
        size = 22,
        color = SC["primary_red"]
      ),
      x = 0.05
    ),
    xaxis = list(
      title = list(
        text = "Relación Deuda/Ingreso (%)",
        font = list(
          family = "Playfair Display",
          size = 14,
          color = SC["dark_red"]
        )
      ),
      tickformat = ".1f",  # Mostrar como nĆŗmeros decimales
      ticksuffix = "%",    # Agregar símbolo % después del número
      gridcolor = SC["light_red"],
      zerolinecolor = SC["light_red"],
      range = c(0, max(Base_datos$dti_porcentaje, na.rm = TRUE) * 1.05)
    ),
    yaxis = list(
      title = list(
        text = "Frecuencia / Densidad",
        font = list(
          family = "Playfair Display", 
          size = 14,
          color = SC["dark_red"]
        )
      ),
      gridcolor = SC["light_red"],
      zerolinecolor = SC["light_red"]
    ),
    shapes = list(
      # LĆ­nea de mediana
      list(
        type = "line",
        x0 = mediana_dti,
        x1 = mediana_dti,
        y0 = 0,
        y1 = max_densidad * 0.95,
        line = list(
          color = SC["gold"],
          dash = "dash",
          width = 2
        )
      ),
      # LĆ­nea de media
      list(
        type = "line",
        x0 = media_dti,
        x1 = media_dti,
        y0 = 0,
        y1 = max_densidad * 0.95,
        line = list(
          color = SC["dark_red"],
          width = 2
        )
      )
    ),
    plot_bgcolor = SC["parchment"],
    paper_bgcolor = "white",
    font = list(family = "Source Serif Pro", size = 12),
    margin = list(l = 80, r = 50, t = 80, b = 100),  # AumentƩ bottom margin para las anotaciones
    hoverlabel = list(
      font = list(family = "Source Serif Pro")
    ),
    legend = list(
      orientation = "h",
      x = 0.5,
      y = -0.2,  # AjustƩ para dar mƔs espacio
      xanchor = "center",
      font = list(family = "Source Serif Pro")
    ),
    annotations = list(
      # Anotación para mediana
      list(
        x = mediana_dti,
        y = max_densidad,
        text = paste("Mediana:", round(mediana_dti, 1), "%"),
        showarrow = FALSE,
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["gold"]
        ),
        bgcolor = "white",
        bordercolor = SC["gold"],
        borderpad = 4,
        borderwidth = 1
      ),
      # Anotación para media
      list(
        x = media_dti,
        y = max_densidad * 0.85,
        text = paste("Media:", round(media_dti, 1), "%"),
        showarrow = FALSE,
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["dark_red"]
        ),
        bgcolor = "white",
        bordercolor = SC["dark_red"],
        borderpad = 4,
        borderwidth = 1
      ),
      # Fuente en la parte inferior
      list(
        x = 1,
        y = -0.3,
        text = paste(
          "Fuente: Dataset Lending Club |",
          "N =", scales::comma(nrow(Base_datos)), "observaciones"
        ),
        showarrow = FALSE,
        xref = "paper",
        yref = "paper",
        xanchor = "right",
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["secondary_red"]
        )
      )
    )
  ) %>%
  config(
    displayModeBar = TRUE,
    modeBarButtonsToRemove = c("pan2d", "lasso2d", "select2d"),
    displaylogo = FALSE
  )

p_dti_interactivo

Esto indica que, en promedio, los solicitantes destinan menos de una quinta parte de sus ingresos al pago de deudas, lo que refleja niveles de endeudamiento controlados en la mayorĆ­a de los casos. Sin embargo, se observan algunos valores altos por encima del 40% que corresponden a individuos con una carga financiera elevada, lo que puede representar un mayor riesgo de incumplimiento.

La forma suavizada del grÔfico evidencia una distribución continua y bien concentrada, lo que sugiere que el comportamiento de esta variable sigue una tendencia general homogénea dentro de la población crediticia.

4.2.3 Distribución del puntaje FICO

library(purrr)
mediana_fico <- median(Base_datos$FICO, na.rm = TRUE)
media_fico <- mean(Base_datos$FICO, na.rm = TRUE)

crear_frame_fico <- function(porcentaje) {
  datos_anim <- Base_datos %>%
    arrange(FICO) %>%
    slice(1:round(n() * porcentaje))
  
  p <- ggplot(datos_anim, aes(x = FICO)) +
    geom_density(
      bw = 15,
      fill = SC["primary_red"],
      color = SC["dark_red"],
      alpha = 0.8,
      linewidth = 1.1
    ) +
    geom_vline(
      xintercept = mediana_fico,
      color = SC["gold"],
      linetype = "dashed", 
      linewidth = 1.2
    ) +
    geom_vline(
      xintercept = media_fico,
      color = SC["accent_red"],
      linetype = "solid",
      linewidth = 1
    ) +
    scale_x_continuous(
      breaks = seq(600, 850, 25),
      limits = c(600, 850)
    ) +
    scale_y_continuous(
      expand = expansion(mult = c(0, 0.05)),
      labels = scales::comma_format()
    ) +
    labs(
      title = paste("Figura 3. Distribución del Puntaje FICO -", round(porcentaje * 100), "% de datos"),
      x = "Puntaje FICO",
      y = "Densidad",
      caption = paste(
        "Mediana:", round(mediana_fico), "|",
        "Media:", round(media_fico), "|",
        "N:", scales::comma(round(nrow(Base_datos) * porcentaje)), "observaciones"
      )
    )
  p + TS() +
    theme(
      plot.title = element_text(
        family = "Playfair Display",
        face = "bold",
        color = SC["primary_red"],
        size = 16,
        hjust = 0.5
      ),
      plot.caption = element_text(
        family = "Source Serif Pro",
        color = SC["secondary_red"],
        size = 10
      )
    )
}

porcentajes <- seq(0.1, 1, by = 0.1)
frames <- map(porcentajes, crear_frame_fico)

temp_files <- map_chr(seq_along(frames), function(i) {
  archivo <- tempfile(fileext = ".png")
  ggsave(archivo, frames[[i]], width = 10, height = 6, dpi = 100, bg = "white")
  archivo
})


animacion_fico <- image_read(temp_files) %>%
  image_animate(fps = 2, optimize = TRUE) %>%
  image_write("animacion_fico.gif")

knitr::include_graphics("animacion_fico.gif")

La distribución del puntaje FICO evidencia una clara concentración de valores entre 660 y 720 puntos, con una mediana cercana a 690 (línea roja).

Esto indica que la mayorĆ­a de los solicitantes poseen un historial crediticio considerado ā€œbuenoā€, aunque no necesariamente ā€œexcelenteā€.

La distribución es ligeramente asimétrica hacia la derecha, lo cual refleja que existen prestatarios con puntajes altos (mayores a 750), pero en menor proporción.

Este comportamiento es esperable, dado que los puntajes mƔs altos suelen corresponder a individuos con un historial crediticio mƔs largo y estable.

4.2.4 Distribución del monto del préstamo

mediana_monto <- median(Base_datos$Monto_prestado, na.rm = TRUE)
media_monto <- mean(Base_datos$Monto_prestado, na.rm = TRUE)

p_monto_interactivo <- plot_ly(Base_datos, x = ~Monto_prestado) %>%
  add_histogram(
    name = "Frecuencia",
    nbinsx = 30,
    marker = list(
      color = SC["primary_red"],
      line = list(color = "white", width = 1)
    ),
    opacity = 0.9,
    hovertemplate = "Monto: %{x:$,.0f}<br>Frecuencia: %{y}<extra></extra>"
  ) %>%
  layout(
    title = list(
      text = "<b>Figura 4. Distribución del Monto de Préstamos</b>",
      font = list(
        family = "Playfair Display",
        size = 22,
        color = SC["primary_red"]
      ),
      x = 0.05
    ),
    xaxis = list(
      title = list(
        text = "Monto del PrƩstamo (USD)",
        font = list(
          family = "Playfair Display",
          size = 14,
          color = SC["dark_red"]
        )
      ),
      tickformat = "$,.0f",
      gridcolor = SC["light_red"],
      zerolinecolor = SC["light_red"],
      range = c(0, max(Base_datos$Monto_prestado, na.rm = TRUE) * 1.05)
    ),
    yaxis = list(
      title = list(
        text = "Frecuencia",
        font = list(
          family = "Playfair Display", 
          size = 14,
          color = SC["dark_red"]
        )
      ),
      gridcolor = SC["light_red"],
      zerolinecolor = SC["light_red"]
    ),
    shapes = list(
      # LĆ­nea de mediana
      list(
        type = "line",
        x0 = mediana_monto,
        x1 = mediana_monto,
        y0 = 0,
        y1 = 1,
        yref = "paper",
        line = list(
          color = SC["gold"],
          dash = "dash",
          width = 2
        )
      ),
      # LĆ­nea de media
      list(
        type = "line",
        x0 = media_monto,
        x1 = media_monto,
        y0 = 0,
        y1 = 1,
        yref = "paper",
        line = list(
          color = SC["dark_red"],
          width = 2
        )
      )
    ),
    plot_bgcolor = SC["parchment"],
    paper_bgcolor = "white",
    font = list(family = "Source Serif Pro", size = 12),
    margin = list(l = 80, r = 50, t = 80, b = 100),
    hoverlabel = list(
      font = list(family = "Source Serif Pro")
    ),
    annotations = list(
      # Anotación para mediana
      list(
        x = mediana_monto,
        y = 1,
        yref = "paper",
        text = paste("Mediana:", scales::dollar(mediana_monto)),
        showarrow = FALSE,
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["gold"]
        ),
        bgcolor = "white",
        bordercolor = SC["gold"],
        borderpad = 4,
        borderwidth = 1
      ),
      # Anotación para media
      list(
        x = media_monto,
        y = 0.9,
        yref = "paper",
        text = paste("Media:", scales::dollar(media_monto)),
        showarrow = FALSE,
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["dark_red"]
        ),
        bgcolor = "white",
        bordercolor = SC["dark_red"],
        borderpad = 4,
        borderwidth = 1
      ),
      # Fuente en la parte inferior
      list(
        x = 1,
        y = -0.15,
        text = paste(
          "Fuente: Dataset Lending Club |",
          "N =", scales::comma(nrow(Base_datos)), "observaciones"
        ),
        showarrow = FALSE,
        xref = "paper",
        yref = "paper",
        xanchor = "right",
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["secondary_red"]
        )
      )
    )
  ) %>%
  config(
    displayModeBar = TRUE,
    modeBarButtonsToRemove = c("pan2d", "lasso2d", "select2d"),
    displaylogo = FALSE
  )

p_monto_interactivo

El histograma del monto solicitado en préstamo presenta una distribución dispersa, con una fuerte concentración de valores entre 5,000 y 15,000, y una mediana cercana a los 12,000.

Esto sugiere que la mayoría de los créditos aprobados corresponden a montos pequeños o medianos, probablemente asociados a consumo o consolidación de deudas.

Se observa tambiƩn la presencia de montos mƔs altos, aunque con menor frecuencia, lo cual es coherente con una polƭtica crediticia que limita el riesgo mediante montos moderados para la mayorƭa de los solicitantes.

4.3 Variable por estado de pago

4.3.1 Puntaje FICO por estado de pago

stats_fico <- Base_datos %>%
  group_by(Estado) %>%
  summarise(
    mediana = median(FICO, na.rm = TRUE),
    q1 = quantile(FICO, 0.25, na.rm = TRUE),
    q3 = quantile(FICO, 0.75, na.rm = TRUE),
    n = n()
  )

p_fico_box_corregido <- plot_ly() %>%
  add_boxplot(
    data = Base_datos %>% filter(Estado == "Pagado"),
    x = "Pagado", 
    y = ~FICO,
    name = "Pagado",
    boxpoints = "outliers",
    marker = list(
      color = SC["primary_red"],
      size = 5,
      opacity = 0.7
    ),
    line = list(
      color = SC["dark_red"],
      width = 2
    ),
    fillcolor = SC["light_red"],
    width = 0.5
  ) %>%
  add_boxplot(
    data = Base_datos %>% filter(Estado == "Incumplido"),
    x = "Incumplido", 
    y = ~FICO,
    name = "Incumplido",
    boxpoints = "outliers",
    marker = list(
      color = SC["accent_red"],
      size = 5,
      opacity = 0.7
    ),
    line = list(
      color = SC["secondary_red"],
      width = 2
    ),
    fillcolor = paste0(SC["accent_red"], "40"),
    width = 0.5
  ) %>%
  layout(
    title = list(
      text = "<b>Figura 5. Distribución del Puntaje FICO por Estado de Pago</b>",
      font = list(
        family = "Playfair Display",
        size = 22,
        color = SC["primary_red"]
      ),
      x = 0.05
    ),
    xaxis = list(
      title = list(
        text = "Estado de Pago",
        font = list(
          family = "Playfair Display",
          size = 14,
          color = SC["dark_red"]
        )
      ),
      gridcolor = SC["light_red"],
      tickfont = list(family = "Source Serif Pro", size = 12)
    ),
    yaxis = list(
      title = list(
        text = "Puntaje FICO",
        font = list(
          family = "Playfair Display", 
          size = 14,
          color = SC["dark_red"]
        )
      ),
      gridcolor = SC["light_red"],
      zerolinecolor = SC["light_red"],
      range = c(600, 850)  # Rango tĆ­pico de FICO
    ),
    plot_bgcolor = SC["parchment"],
    paper_bgcolor = "white",
    font = list(family = "Source Serif Pro", size = 12),
    margin = list(l = 80, r = 50, t = 80, b = 100),
    hoverlabel = list(
      font = list(family = "Source Serif Pro")
    ),
    showlegend = FALSE,
    boxmode = "group",
    annotations = list(
      list(
        x = "Pagado", 
        y = stats_fico$mediana[stats_fico$Estado == "Pagado"],
        text = paste("Mediana:", round(stats_fico$mediana[stats_fico$Estado == "Pagado"])),
        showarrow = FALSE,
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["dark_red"]
        ),
        bgcolor = "white",
        bordercolor = SC["dark_red"],
        borderpad = 4,
        borderwidth = 1
      ),
      list(
        x = "Incumplido",  
        y = stats_fico$mediana[stats_fico$Estado == "Incumplido"],
        text = paste("Mediana:", round(stats_fico$mediana[stats_fico$Estado == "Incumplido"])),
        showarrow = FALSE,
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["dark_red"]
        ),
        bgcolor = "white",
        bordercolor = SC["dark_red"],
        borderpad = 4,
        borderwidth = 1
      ),
      list(
        x = 1,
        y = 600,
        text = paste(
          "Total: N =", scales::comma(nrow(Base_datos)), "<br>",
          "Pagado: N =", scales::comma(stats_fico$n[stats_fico$Estado == "Pagado"]), "<br>",
          "Incumplido: N =", scales::comma(stats_fico$n[stats_fico$Estado == "Incumplido"])
        ),
        showarrow = FALSE,
        xref = "paper",
        yref = "y",
        xanchor = "right",
        align = "right",
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["secondary_red"]
        ),
        bgcolor = "rgba(255,255,255,0.8)",
        bordercolor = SC["light_red"],
        borderwidth = 1,
        borderpad = 4
      ),
      # Fuente
      list(
        x = 1,
        y = -0.2,
        text = "Fuente: Dataset Lending Club",
        showarrow = FALSE,
        xref = "paper",
        yref = "paper",
        xanchor = "right",
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["secondary_red"]
        )
      )
    )
  ) %>%
  config(
    displayModeBar = TRUE,
    modeBarButtonsToRemove = c("pan2d", "lasso2d", "select2d"),
    displaylogo = FALSE
  )

p_fico_box_corregido

El boxplot evidencia una diferencia notable en el puntaje FICO segĆŗn el estado de pago. Los prestatarios que pagan tienden a tener un FICO ligeramente superior, con una mediana cercana a los 700 puntos, mientras que quienes no pagan se concentran alrededor de 680–690 puntos. Esta diferencia confirma que un mejor historial crediticio se asocia con un mayor cumplimiento en los pagos.

4.3.2 ingreso por estado de pago

stats_ingreso <- Base_datos %>%
  group_by(Estado) %>%
  summarise(
    mediana = median(Ingreso, na.rm = TRUE),
    q1 = quantile(Ingreso, 0.25, na.rm = TRUE),
    q3 = quantile(Ingreso, 0.75, na.rm = TRUE),
    iqr = IQR(Ingreso, na.rm = TRUE),
    n = n()
  )

p_box_ingreso_corregido <- plot_ly() %>%
  add_boxplot(
    data = Base_datos %>% filter(Estado == "Pagado"),
    y = ~Ingreso,
    name = "Pagado",
    boxpoints = "outliers",  # Solo mostrar outliers
    marker = list(
      color = SC["primary_red"],
      size = 5,
      opacity = 0.7
    ),
    line = list(
      color = SC["dark_red"],
      width = 2
    ),
    fillcolor = SC["light_red"],
    width = 0.5
  ) %>%
  # Agregar boxplot para "Incumplido"
  add_boxplot(
    data = Base_datos %>% filter(Estado == "Incumplido"),
    y = ~Ingreso,
    name = "Incumplido",
    boxpoints = "outliers",
    marker = list(
      color = SC["accent_red"],
      size = 5,
      opacity = 0.7
    ),
    line = list(
      color = SC["secondary_red"],
      width = 2
    ),
    fillcolor = paste0(SC["accent_red"], "40"),  # Con transparencia
    width = 0.5
  ) %>%
  layout(
    title = list(
      text = "<b>Figura 6. Distribución del Ingreso por Estado de Pago</b>",
      font = list(
        family = "Playfair Display",
        size = 22,
        color = SC["primary_red"]
      ),
      x = 0.05
    ),
    xaxis = list(
      title = list(
        text = "Estado de Pago",
        font = list(
          family = "Playfair Display",
          size = 14,
          color = SC["dark_red"]
        )
      ),
      tickvals = c(0, 1),
      ticktext = c("Pagado", "Incumplido"),
      gridcolor = SC["light_red"],
      tickfont = list(family = "Source Serif Pro", size = 12)
    ),
    yaxis = list(
      title = list(
        text = "Ingreso Anual (USD)",
        font = list(
          family = "Playfair Display", 
          size = 14,
          color = SC["dark_red"]
        )
      ),
      gridcolor = SC["light_red"],
      zerolinecolor = SC["light_red"],
      tickformat = "$,.0f",
      range = c(0, quantile(Base_datos$Ingreso, 0.95, na.rm = TRUE))  # Usar percentil 95 para evitar outliers extremos
    ),
    plot_bgcolor = SC["parchment"],
    paper_bgcolor = "white",
    font = list(family = "Source Serif Pro", size = 12),
    margin = list(l = 80, r = 50, t = 80, b = 100),
    hoverlabel = list(
      font = list(family = "Source Serif Pro")
    ),
    showlegend = FALSE,
    boxmode = "group",
    annotations = list(
      # Anotación para mediana de Pagado
      list(
        x = 0,
        y = stats_ingreso$mediana[stats_ingreso$Estado == "Pagado"],
        text = paste("Mediana:", scales::dollar(stats_ingreso$mediana[stats_ingreso$Estado == "Pagado"])),
        showarrow = FALSE,
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["dark_red"]
        ),
        bgcolor = "white",
        bordercolor = SC["dark_red"],
        borderpad = 4,
        borderwidth = 1
      ),
      # Anotación para mediana de Incumplido
      list(
        x = 1,
        y = stats_ingreso$mediana[stats_ingreso$Estado == "Incumplido"],
        text = paste("Mediana:", scales::dollar(stats_ingreso$mediana[stats_ingreso$Estado == "Incumplido"])),
        showarrow = FALSE,
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["dark_red"]
        ),
        bgcolor = "white",
        bordercolor = SC["dark_red"],
        borderpad = 4,
        borderwidth = 1
      ),
      # Información de resumen
      list(
        x = 1,
        y = 0,
        text = paste(
          "Total: N =", scales::comma(nrow(Base_datos)), "<br>",
          "Pagado: N =", scales::comma(stats_ingreso$n[stats_ingreso$Estado == "Pagado"]), "<br>",
          "Incumplido: N =", scales::comma(stats_ingreso$n[stats_ingreso$Estado == "Incumplido"])
        ),
        showarrow = FALSE,
        xref = "paper",
        yref = "y",
        xanchor = "right",
        align = "right",
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["secondary_red"]
        ),
        bgcolor = "rgba(255,255,255,0.8)",
        bordercolor = SC["light_red"],
        borderwidth = 1,
        borderpad = 4
      ),
      # Fuente
      list(
        x = 1,
        y = -0.25,
        text = "Fuente: Dataset Lending Club",
        showarrow = FALSE,
        xref = "paper",
        yref = "paper",
        xanchor = "right",
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["secondary_red"]
        )
      )
    )
  ) %>%
  config(
    displayModeBar = TRUE,
    modeBarButtonsToRemove = c("pan2d", "lasso2d", "select2d"),
    displaylogo = FALSE
  )

p_box_ingreso_corregido

La comparación del ingreso por estado de pago revela que los prestatarios que cumplen con sus obligaciones presentan una mediana de ingreso ligeramente mĆ”s alta que los morosos. Aunque la dispersión en ambos grupos es amplia, se observa una mayor presencia de valores atĆ­picos elevados en el grupo ā€œPagaā€, lo cual sugiere que los ingresos mĆ”s altos se asocian con una mayor probabilidad de cumplimiento.

4.3.3 Comparación de variables numéricas según estado de pago

library(tidyr)
datos_largos <- Base_datos %>%
  pivot_longer(
    cols = c(Ingreso, Relacion_deuda_ingreso, Monto_prestado, FICO),
    names_to = "variable", 
    values_to = "valor"
  ) %>%
  mutate(
    variable = factor(variable,
      levels = c("Ingreso", "Relacion_deuda_ingreso", "Monto_prestado", "FICO"),
      labels = c("Ingreso Anual", "Relación Deuda/Ingreso", "Monto del Préstamo", "Puntaje FICO")
    )
  )

# Función corregida para crear boxplots estÔticos
crear_boxplot_estatico <- function(var_nombre, titulo_display) {
  
  datos_var <- datos_largos %>% filter(variable == var_nombre)
  
  # Determinar formato
  if (var_nombre == "Ingreso Anual" | var_nombre == "Monto del PrƩstamo") {
    tickformat <- "$,.0f"
  } else if (var_nombre == "Relación Deuda/Ingreso") {
    tickformat <- ".1%"
  } else {
    tickformat <- ",.0f"
  }
  
  # Calcular estadĆ­sticas para anotaciones
  stats <- datos_var %>%
    group_by(Estado) %>%
    summarise(
      mediana = median(valor, na.rm = TRUE),
      n = n()
    )
  
  # Crear el boxplot con datos separados por estado
  p <- plot_ly() %>%
    # Boxplot para Pagado
    add_boxplot(
      data = datos_var %>% filter(Estado == "Pagado"),
      x = "Pagado",
      y = ~valor,
      name = "Pagado",
      boxpoints = FALSE,
      line = list(color = SC["dark_red"], width = 2),
      fillcolor = paste0(SC["primary_red"], "30"),
      marker = list(color = SC["primary_red"]),
      width = 0.5
    ) %>%
    # Boxplot para Incumplido
    add_boxplot(
      data = datos_var %>% filter(Estado == "Incumplido"),
      x = "Incumplido",
      y = ~valor,
      name = "Incumplido",
      boxpoints = FALSE,
      line = list(color = SC["secondary_red"], width = 2),
      fillcolor = paste0(SC["accent_red"], "30"),
      marker = list(color = SC["accent_red"]),
      width = 0.5
    ) %>%
    layout(
      title = list(
        text = titulo_display,  # TƍTULO INDIVIDUAL PARA CADA GRƁFICA
        font = list(
          family = "Playfair Display",
          size = 16,
          color = SC["primary_red"]
        )
      ),
      xaxis = list(
        title = "",
        tickfont = list(family = "Source Serif Pro", size = 11),
        gridcolor = SC["light_red"]
      ),
      yaxis = list(
        title = "",
        tickformat = tickformat,
        gridcolor = SC["light_red"]
      ),
      plot_bgcolor = SC["parchment"],
      paper_bgcolor = "white",
      margin = list(l = 70, r = 30, t = 80, b = 60),  # AumentƩ top margin para el tƭtulo
      showlegend = FALSE,
      # Anotaciones para medianas
      annotations = list(
        list(
          x = "Pagado",
          y = stats$mediana[stats$Estado == "Pagado"],
          text = paste("Mediana:", 
                      ifelse(var_nombre == "Relación Deuda/Ingreso", 
                             scales::percent(stats$mediana[stats$Estado == "Pagado"]),
                             ifelse(var_nombre %in% c("Ingreso Anual", "Monto del PrƩstamo"),
                                    scales::dollar(stats$mediana[stats$Estado == "Pagado"]),
                                    round(stats$mediana[stats$Estado == "Pagado"])))),
          showarrow = FALSE,
          font = list(family = "Source Serif Pro", size = 9, color = SC["dark_red"]),
          bgcolor = "white",
          bordercolor = SC["dark_red"]
        ),
        list(
          x = "Incumplido",
          y = stats$mediana[stats$Estado == "Incumplido"],
          text = paste("Mediana:", 
                      ifelse(var_nombre == "Relación Deuda/Ingreso", 
                             scales::percent(stats$mediana[stats$Estado == "Incumplido"]),
                             ifelse(var_nombre %in% c("Ingreso Anual", "Monto del PrƩstamo"),
                                    scales::dollar(stats$mediana[stats$Estado == "Incumplido"]),
                                    round(stats$mediana[stats$Estado == "Incumplido"])))),
          showarrow = FALSE,
          font = list(family = "Source Serif Pro", size = 9, color = SC["dark_red"]),
          bgcolor = "white",
          bordercolor = SC["dark_red"]
        )
      )
    )
  
  return(p)
}

# Crear cada subplot individualmente con tĆ­tulos especĆ­ficos
plot_ingreso <- crear_boxplot_estatico("Ingreso Anual", "Distribución del Ingreso Anual")
plot_dti <- crear_boxplot_estatico("Relación Deuda/Ingreso", "Distribución de la Relación Deuda/Ingreso") 
plot_monto <- crear_boxplot_estatico("Monto del Préstamo", "Distribución del Monto del Préstamo")
plot_fico <- crear_boxplot_estatico("Puntaje FICO", "Distribución del Puntaje FICO")

# Combinar en subplots
p_comparativo_final <- subplot(
  plot_ingreso, plot_dti, plot_monto, plot_fico,
  nrows = 2,
  shareX = TRUE,
  titleY = TRUE,
  margin = 0.08  # AumentƩ el margen para acomodar los tƭtulos
) %>%
  layout(
    title = list(
      text = "<b>Comparación de Variables por Estado de Pago</b>",
      font = list(
        family = "Playfair Display",
        size = 24,
        color = SC["primary_red"]
      ),
      x = 0.5,
      y = 0.98  # Posicionar el tƭtulo principal mƔs arriba
    ),
    font = list(family = "Source Serif Pro", size = 12),
    annotations = list(
      # Eje X compartido
      list(
        x = 0.5,
        y = -0.1,
        text = "Estado de Pago",
        showarrow = FALSE,
        xref = "paper",
        yref = "paper",
        xanchor = "center",
        font = list(
          family = "Playfair Display",
          size = 14,
          color = SC["dark_red"]
        )
      ),
      # Eje Y compartido
      list(
        x = -0.08,
        y = 0.5,
        text = "Valor",
        textangle = -90,
        showarrow = FALSE,
        xref = "paper",
        yref = "paper",
        xanchor = "center",
        font = list(
          family = "Playfair Display",
          size = 14,
          color = SC["dark_red"]
        )
      ),
      # Fuente
      list(
        x = 1,
        y = -0.15,
        text = "Fuente: Dataset Lending Club",
        showarrow = FALSE,
        xref = "paper",
        yref = "paper",
        xanchor = "right",
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["secondary_red"]
        )
      )
    )
  ) %>%
  config(
    displayModeBar = FALSE,  # DESACTIVAR LA BARRA DE HERRAMIENTAS (estƔtico)
    staticPlot = TRUE        # HACER EL PLOT COMPLETAMENTE ESTƁTICO
  )

p_comparativo_final

Conjuntamente, los boxplots sugieren un patrón coherente: mayor ingreso y mayor puntaje FICO actúan como factores protectores frente al incumplimiento, mientras que una mayor relación deuda/ingreso incrementa el riesgo.

El monto del préstamo por sí solo no explica totalmente el comportamiento (medianas similares), pero su mayor dispersión entre los morosos indica que créditos elevados en prestatarios vulnerables pueden agravar la probabilidad de default.

Para la gestión del riesgo crediticio estas observaciones implican: priorizar la evaluación de DTI y FICO en la toma de decisiones, contemplar límites o condiciones mÔs estrictas para solicitudes con alta DTI, y considerar estrategias de segmentación donde el tamaño del préstamo se ajuste a la capacidad de pago observada.

4.4 Relaciónes

4.4.1 Relación entre puntaje FICO y relación deuda/ingreso

5 Modelos de clasificación

La clasificación es un enfoque fundamental dentro del aprendizaje supervisado cuyo objetivo es asignar observaciones a categorías predefinidas basÔndose en un conjunto conocido de características. Este tipo de modelos aprenden a partir de datos etiquetados, donde cada instancia cuenta con una clase o categoría asociada, para después predecir la clase de nuevas observaciones. La clasificación se aplica en diversos campos y problemas donde es necesario discriminar entre dos o mÔs opciones. A continuación, se detalla la división de los datos y la implementación de cada modelo utilizado para evaluar su desempeño en la clasificación.

5.1 División de los datos

Para el desarrollo de los modelos de clasificación, se utilizó la base balanceada obtenida en la etapa de preparación y limpieza, conformada por 10.000 observaciones distribuidas equitativamente entre las clases ā€œPagaā€ y ā€œNo_pagaā€. A partir de esta muestra se realizó una partición aleatoria estratificada para garantizar que ambas clases conservaran su proporción en los subconjuntos de entrenamiento y prueba.

Con una semilla fija (set.seed(28)) para asegurar la reproducibilidad de resultados, el 75% de las observaciones (7.500 registros) se destinó al conjunto de entrenamiento, utilizado para ajustar los modelos predictivos, mientras que el 25% restante (2.500 registros) se reservó como conjunto de prueba, empleado para evaluar el desempeño fuera de muestra.

Esta proporción 75/25 sigue las recomendaciones habituales en aprendizaje supervisado, ya que permite disponer de suficientes datos para el entrenamiento, al tiempo que mantiene un subconjunto independiente para validar la capacidad de generalización de los modelos. La división se implementó mediante índices aleatorios (index_entrena e index_test), garantizando la representatividad de las variables predictoras (numéricas y categóricas) y evitando sesgos en la evaluación comparativa de los modelos KNN y Logit.

set.seed(0408)

index_muestra <- sample(1:nrow(Base_datos), nrow(Base_datos))
index_entrena <- index_muestra[1:7500]  
index_test <- index_muestra[7501:length(index_muestra)]  

train <- Base_datos[index_entrena, ]
test <- Base_datos[index_test, ]

5.2 Modelo KNN

El modelo K-Nearest Neighbors (KNN) es un algoritmo de aprendizaje supervisado no paramétrico, ampliamente utilizado para problemas de clasificación, donde predice la clase de un nuevo punto de datos basÔndose en la mayoría de clases de sus k vecinos mÔs cercanos en el espacio de características.

Este mĆ©todo opera de manera ā€œperezosaā€, ya que no construye un modelo explĆ­cito durante el entrenamiento, sino que almacena el conjunto de datos y realiza cĆ”lculos de distancia solo en el momento de la predicción, lo que lo hace simple e intuitivo para capturar patrones locales en los datos.

5.2.1 Modelo KNN (class)

El proceso para entrenar y evaluar el modelo K-Nearest Neighbors (KNN) con el paquete class en R se llevó a cabo en varias etapas. Primero, se dividió el conjunto de datos en variables predictoras y variable respuesta explícitamente, seleccionando únicamente variables numéricas estandarizadas para calcular las distancias, dado que la función knn() no admite variables cualitativas directamente o factores como entradas.

Por simplicidad y dadas las características del paquete class, en esta fase no se utilizó la variable cualitativa Proposito.

La inclusión de variables categóricas habría requerido transformarlas a variables numéricas y estandarizarlas, lo cual añade complejidad. Esta decisión permitió enfocar el anÔlisis en las variables cuantitativas. El manejo de variables cualitativas se dejarÔ para fases posteriores o para el uso de paquetes mÔs flexibles.

library(class)
train <- na.omit(train)
test <- na.omit(test)
vars_input <- c("Ingreso", "Relacion_deuda_ingreso", "Monto_prestado", "FICO")
train_input <- train[, vars_input]
test_input  <- test[, vars_input]
train_output <- train$Estado
test_output  <- test$Estado

scaler <- preProcess(train_input, method = c("center", "scale"))
train_input_scaled <- predict(scaler, train_input)
test_input_scaled  <- predict(scaler, test_input)


k_vals <- 1:200
resultado <- data.frame(k = k_vals, precision = NA_real_)  # Inicializar con NA

for (n in k_vals) {
  pred_temp <- knn(
    train = train_input_scaled,
    test = test_input_scaled,
    cl = train_output,
    k = n
  )
  accuracy <- mean(pred_temp == test_output)
  resultado$precision[n] <- ifelse(is.na(accuracy), 0, accuracy)  
}


if (all(is.na(resultado$precision))) {
  stop("Todas las precisiones son NA - revisar los datos")
} else {
  k_optimo <- resultado$k[which.max(resultado$precision)]
  prec_opt <- max(resultado$precision, na.rm = TRUE)
  
  cat("K óptimo:", k_optimo, "| Precisión óptima:", prec_opt, "\n")
}
K óptimo: 157 | Precisión óptima: 0.5972 
pred_knn <- knn(
  train = train_input_scaled, 
  test = test_input_scaled, 
  cl = train_output,
  k = k_optimo
)

Para determinar el valor óptimo de k, se implementó un ciclo for que evaluó la precisión del modelo para diferentes valores de k entre 1 y 100, calculando la proporción de predicciones correctas en el conjunto de prueba. Esta metodología manual permitió identificar el valor de k que maximiza la precisión en el rango.

Se generó un grÔfico que muestra la precisión del modelo en función del valor de k, facilitando la identificación visual del punto óptimo. Con este valor seleccionado, el modelo final se evaluó utilizando métricas como la matriz de confusión y la precisión general, lo que permitió medir su capacidad para clasificar correctamente la variable objetivo.

Grafico_precision_interactivo <- plot_ly(resultado, x = ~k) %>%
  add_lines(
    y = ~precision,
    line = list(
      color = SC["primary_red"],
      width = 3,
      shape = "spline"
    ),
    name = "Precisión",
    hoverinfo = "y+x",
    hovertemplate = "k = %{x}<br>Precisión = %{y:.3f}<extra></extra>"
  ) %>%
  # Puntos de precisión
  add_markers(
    y = ~precision,
    marker = list(
      size = 8,
      color = ~precision,
      colorscale = list(
        c(0, 1),
        c(SC["light_red"], SC["primary_red"])
      ),
      line = list(
        color = "white",
        width = 1.5
      ),
      opacity = 0.9,
      showscale = TRUE,
      colorbar = list(
        title = "Precisión",
        tickformat = ".0%",
        len = 0.5,
        y = 0.5,
        yanchor = "middle"
      )
    ),
    hoverinfo = "text",
    text = ~paste(
      "<b>k =", k, "</b><br>",
      "Precisión: ", scales::percent(precision, accuracy = 0.1), "<br>",
      "Valor exacto: ", round(precision, 4)
    ),
    showlegend = FALSE
  ) %>%
  # Línea vertical para k óptimo
  add_segments(
    x = k_optimo, xend = k_optimo,
    y = 0, yend = max(resultado$precision),
    line = list(
      color = SC["gold"],
      width = 2.5,
      dash = "dash"
    ),
    name = paste("k óptimo =", k_optimo),
    hoverinfo = "none"
  ) %>%
  layout(
    title = list(
      text = "<b>figura 7. Precisión del Modelo KNN según Número de Vecinos (k)</b>",
      font = list(
        family = "Playfair Display",
        size = 22,
        color = SC["primary_red"]
      ),
      x = 0.05
    ),
    xaxis = list(
      title = list(
        text = "NĆŗmero de Vecinos (k)",
        font = list(
          family = "Playfair Display",
          size = 14,
          color = SC["dark_red"]
        )
      ),
      gridcolor = SC["light_red"],
      tickfont = list(family = "Source Serif Pro", size = 11),
      dtick = ifelse(max(resultado$k) - min(resultado$k) > 20, 5, 1),
      range = c(min(resultado$k) - 0.5, max(resultado$k) + 0.5)
    ),
    yaxis = list(
      title = list(
        text = "Precisión",
        font = list(
          family = "Playfair Display", 
          size = 14,
          color = SC["dark_red"]
        )
      ),
      gridcolor = SC["light_red"],
      zerolinecolor = SC["light_red"],
      tickformat = ".0%",
      range = c(min(resultado$precision) - 0.02, max(resultado$precision) + 0.02)
    ),
    plot_bgcolor = SC["parchment"],
    paper_bgcolor = "white",
    font = list(family = "Source Serif Pro", size = 12),
    margin = list(l = 80, r = 80, t = 100, b = 100),
    hoverlabel = list(
      font = list(family = "Source Serif Pro"),
      bgcolor = "white",
      bordercolor = SC["dark_red"]
    ),
    annotations = list(
      # Anotación para k óptimo
      list(
        x = k_optimo,
        y = max(resultado$precision) - 0.015,
        text = paste("<b>k óptimo =", k_optimo, "</b>"),
        showarrow = FALSE,
        font = list(
          family = "Playfair Display",
          size = 14,
          color = SC["dark_red"]
        ),
        bgcolor = SC["gold"],
        bordercolor = SC["dark_red"],
        borderwidth = 2,
        borderpad = 8,
        opacity = 0.9
      ),
      # Información adicional
      list(
        x = 0.02,
        y = 0.98,
        xref = "paper",
        yref = "paper",
        text = paste(
          "<b>MÔxima precisión:</b>", scales::percent(max(resultado$precision), accuracy = 0.1), "<br>",
          "<b>Mejor k:</b>", k_optimo, "<br>",
          "<b>Rango evaluado:</b>", min(resultado$k), "-", max(resultado$k)
        ),
        showarrow = FALSE,
        align = "left",
        font = list(
          family = "Source Serif Pro",
          size = 11,
          color = SC["dark_red"]
        ),
        bgcolor = "rgba(255,255,255,0.8)",
        bordercolor = SC["light_red"],
        borderwidth = 1,
        borderpad = 8
      ),
      # SubtĆ­tulo
      list(
        x = 0.5,
        y = 0.95,
        xref = "paper",
        yref = "paper",
        text = "El valor óptimo de k maximiza la precisión de clasificación en el conjunto de prueba",
        showarrow = FALSE,
        font = list(
          family = "Source Serif Pro",
          size = 13,
          color = SC["secondary_red"]
        )
      ),
      # Fuente
      list(
        x = 1,
        y = -0.15,
        text = "Fuente: Elaboración propia con base en el dataset Lending Club (2007-2018)",
        showarrow = FALSE,
        xref = "paper",
        yref = "paper",
        xanchor = "right",
        font = list(
          family = "Source Serif Pro",
          size = 10,
          color = SC["secondary_red"]
        )
      )
    )
  ) %>%
  config(
    displayModeBar = TRUE,
    modeBarButtonsToRemove = c("pan2d", "lasso2d", "select2d"),
    displaylogo = FALSE,
    toImageButtonOptions = list(
      format = "png",
      filename = "precision_knn",
      width = 1000,
      height = 600
    )
  )

Grafico_precision_interactivo

Luego de analizar la grÔfica de precisión para distintos valores de k, se eligió k = 157, que corresponde al punto donde el modelo alcanza su mayor porcentaje de aciertos en la clasificación sobre el conjunto de prueba 59.7%. Este valor de k fue utilizado para entrenar el modelo y obtener las métricas de desempeño que se presentan.

A continuación, se reportan la matriz de confusión y la tabla de indicadores principales de evaluación para documentar el rendimiento del modelo KNN con este valor de k.

pred_knn_adj <- factor(pred_knn, levels = c("Pagado", "Incumplido"))
test_output_adj <- factor(test_output, levels = c("Pagado", "Incumplido"))

cm <- confusionMatrix(pred_knn_adj, test_output_adj, positive = "Incumplido")

matriz_conf <- as.data.frame(cm$table)

matriz_tabla_mejorada <- matrix(
  c(matriz_conf$Freq[1], matriz_conf$Freq[2],
    matriz_conf$Freq[3], matriz_conf$Freq[4]),
  nrow = 2, byrow = TRUE,
  dimnames = list(
    "Predicción" = c("Pagado", "Incumplido"),
    "Real" = c("Pagado", "Incumplido")
  )
)

tabla_matriz_simple <- as.table(matriz_tabla_mejorada) %>%
  kbl(
    caption = "Matriz de Confusión - Modelo KNN",
    align = c("c", "c", "c"),
    col.names = c("Pagado", "Incumplido")
  ) %>%
  kable_styling(
    bootstrap_options = c("basic"),
    full_width = FALSE,
    font_size = 14,
    position = "center",
    html_font = "Source Serif Pro"
  ) %>%
  add_header_above(
    c(" " = 1, "Valor Real" = 2), 
    bold = TRUE, 
    background = SC["primary_red"], 
    color = "white"
  ) %>%
  row_spec(0, background = SC["dark_red"], color = "white", bold = TRUE) %>%
  row_spec(1:2, background = SC["parchment"]) %>%
  column_spec(1, bold = TRUE, width = "3cm", background = SC["light_red"]) %>%
  footnote(
    general = "Los valores en la diagonal representan las clasificaciones correctas.",
    general_title = "Nota:",
    footnote_as_chunk = TRUE
  )

tabla_matriz_simple
Matriz de Confusión - Modelo KNN
Valor Real
Pagado Incumplido
Pagado 696 514
Incumplido 492 798
Nota: Los valores en la diagonal representan las clasificaciones correctas.
metricas_knn_mejoradas <- data.frame(
  "MƩtrica" = c(
    "Exactitud (Accuracy)",
    "Intervalo de Confianza 95%",
    "Tasa de No Información", 
    "Valor P [Exactitud > TNI]",
    "Kappa de Cohen",
    "Valor P Test de McNemar",
    "Sensibilidad (Recall)",
    "Especificidad",
    "Valor Predictivo Positivo (Precision)",
    "Valor Predictivo Negativo", 
    "Prevalencia",
    "Tasa de Detección",
    "Prevalencia de Detección",
    "Exactitud Balanceada"
  ),
  "Descripción" = c(
    "Proporción total de predicciones correctas",
    "Intervalo de confianza para la exactitud",
    "Tasa al predecir siempre la clase mayoritaria",
    "Significancia estadĆ­stica vs tasa base",
    "Acuerdo ajustado por azar entre observado y predicho",
    "Test de simetría entre clasificaciones erróneas",
    "Capacidad de detectar prƩstamos incumplidos (VP / VP+FN)",
    "Capacidad de detectar prƩstamos pagados (VN / VN+FP)", 
    "Precisión para clase 'Incumplido' (VP / VP+FP)",
    "Precisión para clase 'Pagado' (VN / VN+FN)",
    "Proporción real de préstamos incumplidos",
    "Tasa de detección de incumplimientos (VP / total)",
    "Proporción predicha de incumplimientos (VP+FP / total)",
    "Promedio entre sensibilidad y especificidad"
  ),
  "Valor" = c(
    sprintf("%.4f", cm$overall["Accuracy"]),
    sprintf("(%.4f, %.4f)", cm$overall["AccuracyLower"], cm$overall["AccuracyUpper"]),
    sprintf("%.4f", cm$overall["AccuracyNull"]),
    sprintf("%.2e", cm$overall["AccuracyPValue"]),
    sprintf("%.4f", cm$overall["Kappa"]),
    sprintf("%.5f", cm$overall["McnemarPValue"]),
    sprintf("%.4f", cm$byClass["Sensitivity"]),
    sprintf("%.4f", cm$byClass["Specificity"]),
    sprintf("%.4f", cm$byClass["Pos Pred Value"]),
    sprintf("%.4f", cm$byClass["Neg Pred Value"]),
    sprintf("%.4f", cm$byClass["Prevalence"]),
    sprintf("%.4f", cm$byClass["Detection Rate"]),
    sprintf("%.4f", cm$byClass["Detection Prevalence"]),
    sprintf("%.4f", cm$byClass["Balanced Accuracy"])
  )
)

tabla_metricas_mejorada <- metricas_knn_mejoradas %>%
  kbl(
    caption = "Métricas de Evaluación del Modelo KNN",
    align = c("l", "l", "c"),
    col.names = c("Métrica", "Descripción", "Valor")
  ) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE,
    font_size = 14,
    position = "center",
    html_font = "Source Serif Pro"
  ) %>%
  row_spec(0, background = SC["primary_red"], color = "white", bold = TRUE) %>%
  row_spec(1:nrow(metricas_knn_mejoradas), background = SC["parchment"]) %>%
  column_spec(1, bold = TRUE, width = "3.5cm") %>%
  column_spec(2, width = "6cm") %>%
  column_spec(3, width = "2.5cm") %>%
  footnote(
    general = "VP = Verdadero Positivo, VN = Verdadero Negativo, FP = Falso Positivo, FN = Falso Negativo",
    general_title = "Leyenda:",
    footnote_as_chunk = TRUE
  )

tabla_metricas_mejorada
Métricas de Evaluación del Modelo KNN
Métrica Descripción Valor
Exactitud (Accuracy) Proporción total de predicciones correctas 0.5976
Intervalo de Confianza 95% Intervalo de confianza para la exactitud (0.5781, 0.6169)
Tasa de No Información Tasa al predecir siempre la clase mayoritaria 0.5160
Valor P [Exactitud > TNI] Significancia estadĆ­stica vs tasa base 1.46e-16
Kappa de Cohen Acuerdo ajustado por azar entre observado y predicho 0.1939
Valor P Test de McNemar Test de simetría entre clasificaciones erróneas 0.50791
Sensibilidad (Recall) Capacidad de detectar prƩstamos incumplidos (VP / VP+FN) 0.6186
Especificidad Capacidad de detectar prƩstamos pagados (VN / VN+FP) 0.5752
Valor Predictivo Positivo (Precision) Precisión para clase ā€˜Incumplido’ (VP / VP+FP) 0.6082
Valor Predictivo Negativo Precisión para clase ā€˜Pagado’ (VN / VN+FN) 0.5859
Prevalencia Proporción real de préstamos incumplidos 0.5160
Tasa de Detección Tasa de detección de incumplimientos (VP / total) 0.3192
Prevalencia de Detección Proporción predicha de incumplimientos (VP+FP / total) 0.5248
Exactitud Balanceada Promedio entre sensibilidad y especificidad 0.5969
Leyenda: VP = Verdadero Positivo, VN = Verdadero Negativo, FP = Falso Positivo, FN = Falso Negativo

Los resultados alcanzados por el modelo KNN utilizando el paquete class reflejan un desempeño bastante modesto en la tarea de clasificación entre préstamos pagados y no pagados. La exactitud obtenida, de solo 59.7%, es apenas superior a la que se lograría clasificando siempre la clase mÔs frecuente en la muestra (No Information Rate: 51%), lo que muestra la dificultad inherente al problema y a la información contenida en las variables numéricas utilizadas.

El estadístico Kappa (0.11) es bajo, señalando que el acuerdo entre las predicciones del modelo y el resultado real es solo marginalmente mejor que el azar. Asimismo, tanto la sensibilidad (57.7%) como la especificidad (53.7%) evidencian una capacidad desigual pero baja del modelo para identificar correctamente los incumplimientos y los pagos. Ninguna de las dos clases es clasificada con gran efectividad, lo que refleja limitaciones importantes cuando se busca un sistema confiable para apoyar decisiones en la concesión de pretamos.

Al observar los valores predictivos (positivo: 54.4% para ā€œNo_pagaā€, negativo: 56.9% para ā€œPagaā€), se observa que el modelo tiende a equivocarse en una proporción relevante de casos, ya que casi la mitad de las observaciones podrĆ­an estar erróneamente clasificadas bajo cada etiqueta. AdemĆ”s, el valor de la exactitud balanceada (55.7%) confirma el carĆ”cter modesto y poco robusto del enfoque actual, incluso en un contexto donde las clases han sido equilibradas a propósito.

En concreto, aunque el modelo muestra una ligera capacidad para mejorar la predicción respecto al azar, su potencial real es muy limitado bajo las condiciones y supuestos aplicados.

5.2.2 Modelo KNN (caret)

El ajuste y evaluación del modelo KNN usando el paquete caret se realizó con el objetivo de aprovechar un proceso estandarizado y mÔs flexible. A diferencia de la función pasada de class, caret permite incluir tanto variables numéricas como categóricas (conversión automÔtica a variables dummy) y automatiza la validación cruzada, la selección de hiperparÔmetros y la obtención de métricas clave a través de un solo flujo de trabajo.

Se configuró el control de entrenamiento para usar validación cruzada de 5 pliegues, estimación de probabilidades y optimización según el Ôrea bajo la curva ROC. El modelo fue entrenado sobre el conjunto de entrenamiento considerando las variables ingreso, relacion_deuda_ingreso, monto_prestamo, puntaje_fico y proposito_agrupado, permitiendo así una mayor riqueza informativa respecto al modelo anterior.

La función caret::train() exploró simultÔneamente múltiples valores de k (k = 1 a 150), normalizó las variables e identificó el valor óptimo en función del desempeño en predicción.

Una vez seleccionado el modelo KNN óptimo, se obtuvieron predicciones para el conjunto de prueba, junto con las probabilidades estimadas para cada clase.

ctrl_knn <- trainControl(
  method = "cv",
  number = 5,
  classProbs = TRUE,
  summaryFunction = twoClassSummary
)

modelo_knn <- train(
  Estado ~ .,
  data = train,
  method = "knn",
  trControl = ctrl_knn,
  preProcess = c("center", "scale"),
  tuneLength = 150,
  metric = "ROC"
)

6 Conclusiones

6.1 Hallazgos Principales

  1. Relación inversa significativa entre masa vehicular y eficiencia de combustible
  2. Patrón consistente en la distribución de características técnicas
  3. Validación empírica de los postulados teóricos establecidos

6.2 Implicaciones PrƔcticas

Los resultados obtenidos proporcionan evidencia cuantitativa para el desarrollo de polƭticas de eficiencia energƩtica y diseƱo vehicular optimizado.